<?php

declare(strict_types=1);

/*
 * This file is part of eelly package.
 *
 * (c) eelly.com
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace KDD\SDK;

use PhpParser\BuilderFactory;
use PhpParser\Comment;
use PhpParser\Lexer;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Declare_;
use PhpParser\Node\Stmt\DeclareDeclare;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\ParserFactory;
use Shadon\Exception\ServerException;

/**
 * Class Compiler.
 *
 * @author hehui<runphp@qq.com>
 */
class Compiler
{
    private const TPL = 1;

    public static function compile(string $file): void
    {
        $lexer = new Lexer\Emulative([
            'usedAttributes' => [
                'comments',
                'startLine', 'endLine',
                'startTokenPos', 'endTokenPos',
                'shortArraySyntax',
            ],
        ]);
        $parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7, $lexer);
        $stmts = $parser->parse(file_get_contents($file));
        $factory = new BuilderFactory();
        $nodes = [
            new Declare_([new DeclareDeclare('strict_types', new LNumber(1))]),
        ];
        foreach ($stmts as $item) {
            // namespace
            if ($item instanceof Namespace_) {
                $moduleName = $item->name->parts[2];
                $namespace = __NAMESPACE__.'\\'.$moduleName;
                $node = $factory->namespace($namespace);
                $node->addStmt($factory->use(__NAMESPACE__.'\\Client'));
                foreach ($item->getAttribute('comments') as $comment) {
                    $node->setDocComment(new Comment\Doc("\n".$comment->getText()));
                }
                foreach ($item->stmts as $classStmt) {
                    if ($classStmt instanceof Class_) {
                        // class
                        $classShortName = substr((string) $classStmt->name, 0, -8);
                        $className = $classShortName.'Service';
                        if (class_exists($namespace.'\\'.$className)) {
                            return;
                        }
                        $newClass = $factory->class($className);
                        $newClass->setDocComment(new Comment\Doc(sprintf("\n/**
 * This class has been auto-generated by shadon compiler (%s).
 */", date('Y-m-d H:i:s'))));
                        // methods
                        foreach ($classStmt->stmts as $methodStmt) {
                            if ($methodStmt instanceof ClassMethod && $methodStmt->isPublic()) {
                                $methodName = (string) $methodStmt->name;
                                $returnType = $methodStmt->getReturnType();
                                if (null === $returnType) {
                                    throw new ServerException(sprintf('method `%s` must have return type', $methodName));
                                }
                                $newMethod = $factory
                                    ->method($methodName)
                                    ->makePublic()
                                    ->makeStatic()
                                    ->setReturnType($returnType);
                                $docs = $methodStmt->getAttribute('comments');
                                if (null === $docs) {
                                    $strr = str_repeat('=', 100);
                                    $newMethod->setDocComment(new Comment\Doc(sprintf("\n/** 
 * %s                                    
 * %sWARNING: lost comments
 * %s
 */", $strr, str_repeat(' ', 40), $strr)));
                                } else {
                                    foreach ($docs as $doc) {
                                        $newMethod->setDocComment($doc);
                                    }
                                }
                                // args
                                $args = [
                                    new \PhpParser\Node\Arg(new \PhpParser\Node\Scalar\String_(
                                        sprintf('/%s/internal/%s/%s?tpl=%d', lcfirst($moduleName), lcfirst($classShortName), $methodName, self::TPL))
                                    ),
                                ];
                                // params
                                $argsBag = [];
                                foreach ($methodStmt->getParams() as $param) {
                                    $argsBag[] = new \PhpParser\Node\Expr\ArrayItem(
                                        new \PhpParser\Node\Expr\Variable($param->var->name),
                                        new \PhpParser\Node\Scalar\String_($param->var->name)
                                     );

                                    $newMethod->addParam($param);
                                }
                                $args[] = new \PhpParser\Node\Arg(new \PhpParser\Node\Expr\Array_($argsBag));
                                $args[] = $returnType instanceof Identifier ?
                                    new String_($returnType->name) :
                                    new \PhpParser\Node\Arg(
                                        new ClassConstFetch($returnType, 'class')
                                    );
                                if ($returnType instanceof Identifier && 'void' == $returnType->name) {
                                    $newMethod->addStmt(new StaticCall(new Name('Client'), new Identifier('requestApi'), $args));
                                } else {
                                    $newMethod->addStmt(new Return_(
                                        new StaticCall(new Name('Client'), new Identifier('requestApi'), $args)
                                    ));
                                }
                                $newClass->addStmt($newMethod);
                            }
                        }
                        $node->addStmt($newClass);
                    } elseif ($classStmt instanceof Use_) {
                        // use
                        if ('Module' != $classStmt->uses[0]->name->parts[1]) {
                            // sdk 不应该依赖module
                            $node->addStmt($classStmt);
                        }
                    }
                }
                $nodes[] = $node->getNode();
            }
        }
        // ready
        $prettyPrinter = new \PhpParser\PrettyPrinter\Standard(['shortArraySyntax' => true]);
        $newCode = $prettyPrinter->prettyPrintFile($nodes);
        $sdkFile = __DIR__.'/'.$moduleName.'/'.$className.'.php';
        file_put_contents($sdkFile, $newCode);
    }
}
